package com.yingsheng.bi.hks.template.processor.impl;

import com.yingsheng.bi.hks.template.element.NotEmptyElementProcessor;
import com.yingsheng.bi.hks.template.events.AdviceEvent;
import com.yingsheng.bi.hks.template.events.ElementEvent;
import com.yingsheng.bi.hks.template.events.Event;
import com.yingsheng.bi.hks.template.processor.ElementProcessor;
import com.yingsheng.bi.hks.template.processor.InnerProcessor;
import com.yingsheng.bi.hks.template.utils.JacksonUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;

public class FieldElementProcessor extends InnerProcessor implements ApplicationContextAware {

    protected final Log logger = LogFactory.getLog(this.getClass());

    private ApplicationContext applicationContext;

    private void elementProcess(AdviceEvent adviceEvent, Object ref, DtoElement dtoElement) {
        if (ref instanceof ElementProcessor) {
            FieldElement fieldElement = dtoElement.getField();
            Field field = fieldElement.getField();
            String name = field.getName();
            Object beforeValue = adviceEvent.getData(name);
            ElementEvent elementEvent = new ElementEvent(name, adviceEvent);
            elementEvent.setAttrs(fieldElement.getAttrs());
            elementProcess(ref, elementEvent);
            Object afterValue = adviceEvent.getData(name);
            if (beforeValue != afterValue) {
                setValue(fieldElement.getField(), dtoElement.getDto(), name, afterValue);
            }
        }
    }

    @Override
    public void execInner(Event event) {
        if (event instanceof AdviceEvent && this.applicationContext != null) {
            AdviceEvent adviceEvent = (AdviceEvent) event;
            Object input = adviceEvent.getInput();
            if (input instanceof DtoElement) {
                DtoElement dtoElement = (DtoElement) input;
                FieldElement fieldElement = dtoElement.getField();
                Object ref = fieldElement.getName();
                Field field = fieldElement.getField();
                String name = field.getName();
                if (ref != null) {
                    if (ref instanceof String && StringUtils.hasText((String) ref)) {
                        ref = getElementProcessor((String) ref);
                        fieldElement.setName(ref);
                    }
                    if (ref instanceof Object[]) {
                        for (Object obj : (Object[]) ref) {
                            elementProcess(adviceEvent, obj, dtoElement);
                        }
                    } else {
                        elementProcess(adviceEvent, ref, dtoElement);
                    }
                }
                Class fieldClazz = field.getType();
                if (!BeanUtils.isSimpleProperty(fieldClazz) && fieldClazz != Object.class) {
                    Object value = getValue(field, dtoElement.getDto());
                    if (value != null) {
                        AdviceEvent fieldEvent = new AdviceEvent();
                        fieldEvent.setRequest(event);
                        if (isMap(fieldClazz)) {
                            Type[] mapTypes = getGenericType(field.getGenericType());
                            if (!ObjectUtils.isEmpty(mapTypes)) {
                                if (mapTypes[1] instanceof Class) {
                                    Class mapValueClass = (Class) mapTypes[1];
                                    if (!BeanUtils.isSimpleProperty(mapValueClass) && mapValueClass != Object.class) {
                                        Map map = (Map) value;
                                        Map mapNew = new HashMap();
                                        Set<Map.Entry> entrySet = map.entrySet();
                                        for (Map.Entry entry : entrySet) {
                                            Object mapValue = entry.getValue();
                                            fieldEvent.setInput(mapValue);
                                            fieldEvent.putAll(JacksonUtils.obj2Map(mapValue));
                                            super.execInner(fieldEvent);
                                            mapNew.put(entry.getKey(), fieldEvent.getDataMap());
                                        }
                                        adviceEvent.setData(name, mapNew);
                                    }
                                }
                            }
                        } else if (isArray(fieldClazz)) {
                            Class arrayComponentType = fieldClazz.getComponentType();
                            if (arrayComponentType != Object.class) {

                                Object[] array = (Object[]) value;
                                int length = array.length;
                                Map[] arrayNew = new Map[length];
                                for (int i = 0; i < length; i++) {
                                    Object item = array[i];
                                    fieldEvent.setInput(item);
                                    fieldEvent.putAll(JacksonUtils.obj2Map(item));
                                    super.execInner(fieldEvent);
                                    arrayNew[i] = fieldEvent.getDataMap();
                                    fieldEvent.setDataMap(null);
                                }
                                adviceEvent.setData(name, arrayNew);
                            }
                        } else if (isCollection(fieldClazz)) {
                            Type[] collectionTypes = getGenericType(field.getGenericType());
                            if (!ObjectUtils.isEmpty(collectionTypes)) {
                                if (collectionTypes[0] instanceof Class) {
                                    Class collectionEleClass = (Class) collectionTypes[0];
                                    if (!BeanUtils.isSimpleProperty(collectionEleClass) && collectionEleClass != Object.class) {
                                        Collection collection = (Collection) value;
                                        Collection collectionNew = new ArrayList(collection.size());
                                        for (Object item : collection) {
                                            fieldEvent.setInput(item);
                                            fieldEvent.putAll(JacksonUtils.obj2Map(item));
                                            super.execInner(fieldEvent);
                                            collectionNew.add(fieldEvent.getDataMap());
                                            fieldEvent.setDataMap(null);
                                        }
                                        adviceEvent.setData(name, collectionNew);
                                    }
                                }
                            }
                        } else {
                            if (!fieldClazz.getCanonicalName().startsWith("java")) {
                                fieldEvent.setInput(value);
                                fieldEvent.putAll(JacksonUtils.obj2Map(value));
                                super.execInner(fieldEvent);
                                adviceEvent.setData(name, fieldEvent.getDataMap());
                                fieldEvent.setDataMap(null);
                            }
                        }
                    }

                }
            }
        } else {
            super.execInner(event);
        }
    }

    private boolean isMap(Class<?> clazz) {
        Assert.notNull(clazz, "Class must not be null");
        return Map.class.isAssignableFrom(clazz);
    }

    private boolean isArray(Class<?> clazz) {
        Assert.notNull(clazz, "Class must not be null");
        return clazz.isArray();
    }

    private boolean isCollection(Class<?> clazz) {
        Assert.notNull(clazz, "Class must not be null");
        return Collection.class.isAssignableFrom(clazz);
    }

    private Type[] getGenericType(Type type) {
        if (type instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) type;
            return parameterizedType.getActualTypeArguments();
        }
        return null;
    }

    private Object getValue(Field field, Object target) {
        try {
            ReflectionUtils.makeAccessible(field);
            return field.get(target);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return null;
        }
    }

    private void setValue(Field field, Object target, String name, Object value) {
        try {
            ReflectionUtils.makeAccessible(field);
            field.set(target, value);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            throw new RuntimeException(target.getClass().getSimpleName() + " Field " + name + " set value error");
        }
    }

    private void elementProcess(Object ref, ElementEvent elementEvent) {
        if (ref != null) {
            if (ref instanceof ElementProcessor) {
                elementProcess((ElementProcessor) ref, elementEvent);
            } else if (ref instanceof ElementProcessor[]) {
                elementProcess((ElementProcessor[]) ref, elementEvent);
            }
        }
    }

    private void elementProcess(ElementProcessor[] elementProcessors, ElementEvent elementEvent) {
        if (!ObjectUtils.isEmpty(elementProcessors)) {
            for (ElementProcessor elementProcessor : elementProcessors) {
                elementProcess(elementProcessor, elementEvent);
            }
        }
    }

    private void elementProcess(ElementProcessor elementProcessor, ElementEvent elementEvent) {
        if (elementProcessor != null) {
            String key = elementEvent.getName();
            Object value = elementEvent.getData(key);
            elementEvent.setInput(value);
            elementProcessor.exec(elementEvent);
            if (elementEvent.getOutput() != null) {
                elementEvent.setData(key, elementEvent.getOutput());
            }
            elementEvent.reset();
        }
    }

    @Override
    public int getOrder() {
        return Integer.MIN_VALUE + 5;
    }

    private Object getElementProcessor(String name) {
        if (StringUtils.hasText(name)) {
            String[] names = name.split("[,;]");
            if (!ObjectUtils.isEmpty(names)) {
                if (names.length == 1) {
                    return getElementProcessorBeanFlexible(names[0]);
                } else {
                    Object[] elementProcessors = new Object[names.length];
                    for (int i = 0; i < names.length; i++) {
                        elementProcessors[i] = getElementProcessorBeanFlexible(names[i]);
                    }
                    return elementProcessors;
                }
            }
        }
        return null;
    }

    private Object getElementProcessorBeanFlexible(String name) {
        if (StringUtils.hasText(name)) {
            Object bean = getElementProcessorBean(name);
            if (bean == null && !name.endsWith("ElementProcessor")) {
                bean = getElementProcessorBean(name + "ElementProcessor");
            }
            return bean;
        }
        return null;
    }

    private Map<String, ElementProcessor> elementProcessors;

    public FieldElementProcessor() {
        elementProcessors = new HashMap<>();
        elementProcessors.put("NotEmpty", new NotEmptyElementProcessor());
    }

    private Object getElementProcessorBean(String name) {
        if (StringUtils.hasText(name)) {
            ElementProcessor elementProcessor = elementProcessors.get(name);
            if (elementProcessor != null) {
                return elementProcessor;
            }
            try {
                return this.applicationContext.getBean(name, ElementProcessor.class);
            } catch (Exception e) {
                logger.debug(e.getMessage());
            }
        }
        return null;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}
